package whendone

/**
  WhenDone application. 
  WhenDone object contains mainly handlers for different user inputs.
  @author Henrik Huttunen
*/
object WhenDone extends Application {

  import Configuration._
  import Filtering._
  import PrettyPrinter._
  import Helpers._
  import CommandHistory._

  implicit val io = new SwingIO{}
  val defaultSavefile = new java.io.File("default.bin") 
  var savefile = defaultSavefile
  val backupDirectory = new java.io.File("backup") 

  def setCurrentSavefile(file: java.io.File) = { savefile = file }

  /** Loads existing project */
  def loadMainProject(file: java.io.File): Project = {
    /** Loads project from file and prints info about it */
    def loadFromFile(file: java.io.File) = {
      val project = Project.loadFromFile(file)
      io.print("Projects loaded from file '")
      printlnWith(file.toString * Fore(java.awt.Color.GREEN) * Back(java.awt.Color.BLACK))
      project
    }
		// loads project and backups it
    val project = loadFromFile(file)
    val backupFile = newBackupFileFor(backupDirectory.toString, file)      
    Project.saveToFile(project)(backupFile)
    project
  }
  /** Creates totally new project to a file */
  def createNewMainProject(file: java.io.File): Project = {
    val project = new Project(None, "root")
    Project.saveToFile(project)(file)
    printlnWith("New data file '" | file.toString * Fore(java.awt.Color.GREEN) * Back(java.awt.Color.BLACK) | "' created.\n")
    project    
  }
  /** The root project, (grand)parent of all projects */
  var MainProject = {
    if(defaultSavefile.exists) loadMainProject(defaultSavefile)
    else createNewMainProject(defaultSavefile)
  }
  /** Sets project as MainProject and currentProject) */
	def setMainProject(project: Project) = {
   MainProject = project
   currentProject = project
  }
  /** Saves MainProject to file */
  def saveToFile(file: java.io.File) = Project.saveToFile(MainProject)(file)
  /** Project that user is currently located at, and to what many of the commands work relatively. */ 
  var currentProject: Project = MainProject
  /** Is it the end of repeat-eval-print-loop */
  var end = false 
  /** History of given commands */
  var commandHistory = CommandHistory.emptyHistory // history of user given commands
  
  /** do command */
  def handleDone = currentProject.parent match {
    case None         => io.println("Cannot apply 'done' on '" + currentProject.name + "'.")
    case Some(parent) => currentProject = parent
  }
  /** Finds project starting from optionally given project, and diving into subprojects determined by parts.
      Parts form a path of projects, e.g. x/y/z, when parts = List(x, y, z) 
  */
  def findProject(project: Option[Project], parts: List[String]): Option[Project] = {
    /** Starts searching a project only using parts from MainProject, when no start project is given */
    def withNoStartProject = parts match {
      case head :: rest if MainProject.name == head => findProject(Some(MainProject), rest)
      case _ => None
    }
    /** Starts the searching from a given project */ 
    def withStartProject(project: Project) = parts match {
      case Nil => Some(project)
      case part :: rest => project.projects.find(x => x.name == part) match {
        case None => None
        case Some(subProject) => findProject(Some(subProject), rest)
      }
    }
    project match {
      case None           => withNoStartProject 
      case Some(project)  => withStartProject(project)
    }
  }
  /** Do command, which is relative to the currentProject.
      Goes to a subproject, or creates new, determined by whether project 'name' already exists. */   
  def handleDoProject(name: String) = {
    /** Project is determined by path, i.e. it's form of parent/subproject */
    def doWithPath = {
      findProject(Some(currentProject), name.split("/").toList) match {
        case None          => io.println("There's no project that has path '" + name + "'.")
        case Some(project) => currentProject = project
      }      
    }
    /** Project should be subproject */
    def doDirectSubProject = {
      currentProject.projects.find(x => x.name == name) match {
        case None => {
          io.println("Project '" + name + "' doesn't exist.")
          io.println("Do you want to create it? (y for yes).")
          if(io.readLine == "y"){
            currentProject = currentProject.addSubProject(name)
            saveToFile(savefile)
          }
          else io.println("Project '" + name + "' was not created.")
        }
        case Some(project) => currentProject = project     
      }
    }
    if(name.contains("/")) doWithPath else doDirectSubProject
  }
  /** Adds log entry to the current project */
  def handleAddEntry(description: String) = {
    currentProject.addEntry(description)
    io.println("Entry added.")
    saveToFile(savefile)
  }
  /** Given command was not found */
  def handleNotFound(command: String) = io.println("Command '" + command + "' not found. Type 'help' for commands.")
  /** Shows direct subprojects relative to the currentProject */
  def handleShowProjects = { showProjects(currentProject, Some(1), false) }
  /** Shows log entries of the currentProject. */
  def handleShowEntries = {
    for(project <- currentProject.entries) 
      io.println(" [" + project.date + "] " + project.description)
  }
  /** Shows help. */
  def handleHelp = printHelp(io)
  /** Sets currentProject as MainProject. */
  def handleRoot = { currentProject = MainProject }
  /** Shows projects starting from project, path is maxDeepness deep at maximum.'
      showGrandParent determines whether topmost project is shown. */ 
  def showProjects(project: Project, maxDeepness: Option[Int], showGrandParent: Boolean) = {
    val max = maxDeepness getOrElse scala.Math.MAX_INT
    def showProjects(project: Project) = {
      val fork = "|-- "
      def printProject(project: Project, prefix: String, deep: Int): Unit = {
        if(deep <= max){
          var prefixAdd = List.make(fork.size, " ").mkString
          if(showGrandParent || deep > 0){
            io.print(prefix + fork)
            printStyled(project)
            io.print("\n")
          }else prefixAdd = ""
          for(subProject <- project.projects) printProject(subProject, prefix + prefixAdd, deep + 1)
        }
      }
      printProject(project, " ", 0)
    }
    showProjects(project)
  }
  /** Shows all project. */
  def handleShowAllProjects = { showProjects(MainProject, None, false) }
  
  /** Shows filtered projects as a tree structure. */
  def showProjects(projects: List[Project], filters: List[String]): Unit = {
    /** Prefix._1 is accumulated prefix for knowing the right horizontal location.
        Prefix._2 is the length of last horizontal print, which is is added only for the next level.
        projects._1 contains (project, and its path) */ 
    def showProjects(cursor: Project, prefix: (String, String), projects: List[(Project, List[String])]): Unit = {
      for(group <- divideProjects(projects filter (!_._2.isEmpty))){
        val projectName = group.head._2.head // one of the groups' path name
        val currentCursor = findProject(Some(cursor), List(projectName)).get
        val separator = "|-- "
        io.print("\n" + prefix._1 + separator)
        printStyled(projectName, filters)
        io.print(" ")
        printAttributes(currentCursor)
        showProjects(currentCursor, (prefix._1 + toSpaces(separator), toSpaces(projectName)), cutHeadOff(group))
      }           
    }    
    showProjects(MainProject, ("", ""), projects.map{x => (x, project2pathchain(x).drop(1).map(_.name))}) // drops root prefix    
    io.println("")
  }
  /** Shows projects that satisfy given filters relative to currentProject. */
  def handleShowFilter(filter: String) = {
    val filters = filter.split(" ").toList
    val filtered = for (project <- filterContains(currentProject, filters)) yield project
    showProjects(filtered.removeDuplicates, filters)
  }
  /** Shows info. */
  def handleInfo = printInfo(io)
  /** Deletes currentProject and its subprojects, unless currentProject is MainProject (then only subprojects.). */
  def handleDelete = {
    def deleteMainProject = {
      io.println("Are you sure you want to delete '" + MainProject.name + "'s all subprojects? (y for yes).")
      if(io.readLine == "y"){
        io.println("Project '" + MainProject.name + "'s subprojects deleted.")
        for(project <- MainProject.projects) Project.delete(project)
        saveToFile(savefile)        
      }
      else io.println("Delete aborted.")      
    }
    def deleteCurrentProject = {
      io.println("Are you sure you want to delete '" + currentProject.name + "', and all of its subprojects? (y for yes).")
      if(io.readLine == "y"){
        io.println("Project '" + currentProject.name + "' deleted.")
        currentProject = Project.delete(currentProject).get
        saveToFile(savefile)        
      }
      else io.println("Delete aborted.")      
    }
    if(currentProject == MainProject) deleteMainProject
    else deleteCurrentProject
  }
  /** Renames currentProject to newName. */    
  def handleRename(newName: String) = {
    if(currentProject == MainProject) io.println("Cannot rename '" + MainProject.name + "'.")
    else if(newName.size == 0) io.println("Name length must be nonzero.")
    else if(newName.contains("/")) io.println("Name is not allowed to contain '/'.")
    else if(currentProject.name == newName)
      io.println("The name of the current project is already '" + newName + "'.")
    else{
      val Some(parent) = currentProject.parent
      parent.projects.find(x => x.name == newName) match {
        case None => 
          currentProject.name = newName
          saveToFile(savefile)
        case Some(project) => io.println("Project '" + newName + "' already exists.")
      }
    }
  }
  /** Go command. Sets currentProject to project that satisfy filter, if unambiguous is found.
      Works globally starting from MainProject. */
  def handleGlobalGo(filter: String) = goTo(MainProject, filter)
  /** Lo command. Sets currentProject to project that satisfy filter, if unambiguous is found. 
      Works locally starting from currentProject. */
	def handleLocalGo(filter: String) = goTo(currentProject, filter)
  
  /** Sets the currentProject to project that satisfy filter, if unambigous is found. */
  def goTo(startProject: Project, filter: String) = {
    def setAsCurrentProject(path: String) = {
      findProject(None, path.split("/").toList) match {
        case None => // do nothing
        case Some(project) => currentProject = project
      }
    }
    val filters = filter.split(" ").toList
    val projects = filterContains(startProject, filters)
    projects.size match {
      case 0 => io.println("There's no project that matches '" + filter + "'.")
      case 1 => currentProject = projects.head
      case _ => showAndSelectAmbiguous(projects, filters, setAsCurrentProject _)
    }
  }
  /** Adds the given attribute to currentProject. */
  def handleAddAttribute(attribute: String) = {
    if(currentProject.attributes.exists(_ == attribute))
      io.println("Project already has attribute '" + attribute + "'.")
    else{
      currentProject.addAttribute(attribute)
      io.println("Attribute '" + attribute + "' set.")
      saveToFile(savefile)
    }
  }
  /** Removes the given attribute from currentProject. */
  def handleRemoveAttribute(attribute: String) = {
    if(!currentProject.attributes.exists(_ == attribute))
      io.println("Project doesn't have attribute '" + attribute + "'.")
    else{
      currentProject.removeAttribute(attribute)
      io.println("Attribute '" + attribute + "' removed.")
      saveToFile(savefile)
    }  
  }
  /** Shows all (sub)projects of currentProject) */
  def handleShowCommand = { showProjects(currentProject, None, false) }
  /** Quits program. */
  def handleQuit = { 
    end = true
    io.close
  }
  /** Whether first argument is (grand)parent of the second argument. */
  def isSubProject(project1: Project, project2: Project): Boolean = {
    project1 == project2 || (project1.projects.map(isSubProject(_, project2))).exists (x => x) 
  }
  /** Shows projects that all match filters, and let the user select non-ambiguous one. */ 
  def showAndSelectAmbiguous(projects: List[Project], filters: List[String], selectAmbiguousProject: String => Unit) = {
    io.println("Ambiguous matches:")
    showProjects(projects, filters)    
    io.setCommandHistory(projects.foldLeft(emptyHistory)((history, project) => addToCommandHistory(history, nameChain(project).mkString("/"))))
    io.println("\nUse up and down arrow keys to browse the ambiguous matches. Press enter to select. (Invalid selection aborts).")
    selectAmbiguousProject(io.readLine)
    io.setCommandHistory(commandHistory)    
  }
  /** Move currentProject to the given path. */
  def selectMovableProject(path: String) = {
    findProject(None, path.split("/").toList) match {
      case None =>
      case Some(project) => {
        if(isSubProject(currentProject, project)) 
          io.println("'"+ currentProject.name + "' cannot be moved to its subproject '" + project.name + "'.")
        else Project.moveProject(currentProject, project)
      }
    }    
  }
  /** Move currentProject to determined by path, which can be exact path name or filters. */ 
  def moveProjectMoveByFilter(path: String) = {
    val filters = path.split("/").toList
    val projects = filterContains(MainProject, filters) filter(!isSubProject(currentProject, _))
    projects.size match {
      case 0 => io.println("There's no project that matches '" + path + "'.")
      case 1 => 
        val targetName = nameChain(projects.head).mkString("/")
      	io.println("Do you want to move '" + currentProject.name + "' to '" + targetName + "'? (y for yes).")
        if(io.readLine == "y"){
          Project.moveProject(currentProject, projects.head)
          saveToFile(savefile)
        }
        else io.println("Move command aborted.")
      case _ => showAndSelectAmbiguous(projects, filters, selectMovableProject _)
    }     
  }
  /** Moves project to place which is determined by path, which is either exact path, or filter */ 
  def moveProjectTo(pathStart: Option[Project], path: String) = {
    val parts = path.split("/").toList
    if(currentProject == MainProject)
      io.println("Cannot move '" + MainProject.name + "'.")
    else findProject(pathStart, parts) match {
      case None => moveProjectMoveByFilter(path) 
      //io.println("There's no project that has path '" + path + "'.")
      case Some(newParent) =>
        if(currentProject == newParent)
          io.println("Project '" + currentProject.name + " is already in '" + path + "'")
        if(isSubProject(currentProject, newParent))
          io.println("Cannot move '" + currentProject.name + "' to '" + newParent.name + ", because latter is subproject of former.")   
        else if(newParent.projects.exists(_.name == currentProject.name))
          io.println("There is already a project '" + currentProject.name + "'in path '" + path + "'.")
        else {
          Project.moveProject(currentProject, newParent)
          saveToFile(savefile)
        }
    }
  }
  /** moveto command. Moves the currentProject to place determined by 'path'. */
  def handleMoveTo(path: String) = moveProjectTo(None, path)
  
  /** Moves project from its parent to its parent's parent. */
  def handleMoveUp = {
    currentProject.parent match {
      case None         => io.println("Cannot move up -- " + currentProject.name + " has no parent.")
      case Some(parent) => parent.parent match {
        case None               => io.println("Cannot move up -- " + currentProject.name + "'s parent has no parent.")
        case Some(grandparent)  =>
          if(grandparent.projects.exists(_.name == currentProject.name))    
            io.println("There is already a project '" + currentProject.name + " in '" + grandparent.name + "'.")
          else {
            Project.moveProject(currentProject, grandparent)
            saveToFile(savefile)
          }
      }
    }
  }
  // temporary hack for own use for setting width and height of swing window :)
  def handleSetWidth(width: String) = io.setWidth(Integer.parseInt(width)) 
  def handleSetHeight(height: String) = io.setHeight(Integer.parseInt(height))
  
  /** load command. Loads project from given filename + ".bin". */
  def handleLoad(filename: String) = {
    val file = new java.io.File(filename + ".bin")
    try {
      if(file exists){
        setMainProject(loadMainProject(file))
        setCurrentSavefile(file) // if exception is thrown in loadFromFile, this won't be executed
      }else{
        io.println("File '" + file.getName + "' doesn't exist. Do you want to create it? (y for yes).")
        if(io.readLine == "y"){
          setMainProject(createNewMainProject(file))
          setCurrentSavefile(file)
        }
        else io.println("Load aborted.")
      }
    }catch{
      case _ => io.println("Couldn't load from file '" + filename + "'.")
    }
  }
  /** General handling for user input. Selects specific handler to do the real work. */
  def handle(input: String) = {
    commandHistory = addToCommandHistory(commandHistory, input)
    /** A helper for commands that needs arguments, sees whether input starts with command.
        If so, calls the given fun with rest of the input. 
        Returns true iff command was matched. */
    def parametrised(command: String, fun: String => Unit) = {
      if(input.startsWith(command) && input.size > command.size){
        fun(input.substring(command.size, input.size))
        true
      }else false
    }
    if(parametrised(goCommand, handleGlobalGo)) {}
    else if(parametrised(loCommand, handleLocalGo)) {}
    else if(parametrised(didCommand, handleAddEntry)) {}
    else if(parametrised(doCommand, handleDoProject)) {}
    else if(parametrised(loadCommand, handleLoad)) {}
    else if(parametrised(renameCommand, handleRename)) {}
    else if(parametrised(moveToCommand, handleMoveTo)) {}
    else if(parametrised(showFilterCommand, handleShowFilter)) {}
    else if(parametrised(addAttributeCommand, handleAddAttribute)) {}
    else if(parametrised(removeAttributeCommand, handleRemoveAttribute)) {}
    
    else if(parametrised("width ", handleSetWidth)){} // temporary hack for own use
    else if(parametrised("height ", handleSetHeight)){} // temporary hack for own use
    
    else input match{
      case `moveUpCommand`          => handleMoveUp
      case `deleteCommand`          => handleDelete
      case `doneCommand`            => handleDone
      case `quitCommand`            => handleQuit
      case `showProjectsCommand`    => handleShowProjects
      case `showEntriesCommand`     => handleShowEntries
      case `showAllProjectsCommand` => handleShowAllProjects
      case `showCommand`            => handleShowCommand
      case `helpCommand`            => handleHelp
      case `rootCommand`            => handleRoot
      case `infoCommand`            => handleInfo
      case _                        => handleNotFound(input)
    }
  }
  /** Program main. Prints welcome message, runs read-eval-print-loop until end variable is set to true. */
  override def main(args: Array[String]) = {
    printWelcomeInfo
    while(!end){
      printCurrentProject(currentProject)
      io.print(" ")
      io.setCommandHistory(commandHistory)
      handle(io.readLine)
    }
  }
}
